Extension is workflow-class identity; nothing carries it per run - #4
Merged
Conversation
start() stamps {subject_type, subject_id, extension} as DBOS custom
workflow attributes at enqueue (subject_id always a string), so every
runs-for-a-subject query reads workflow_status itself through one
EXISTS predicate (subject_filter). The subject, extension, and input
columns leave durable_runs; the row keeps only the facts DBOS has no
slot for: the gate ask, failure, and timestamps.
Events and signals take subject/extension from the workflow's own
arguments (replay-deterministic), the park notification receives the
subject from the parking workflow, and the transcript-files route names
its own mount instead of reading the run's stamp. The migration runs
DBOS's system migrations first so the attributes backfill from
durable_runs always has its column, whatever the host booted last.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every host already runs dbos 2.26, so the attributes column exists before this history does, and runs predating the deploy stay unkeyed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run.list_for_subject grows a kind filter; build's newest-scope lookup and Scope.parked_for compose it instead of importing the raw subject_filter predicate from the durable engine. Per (kind, subject) the queue dedup makes runs strictly sequential, so newest-first holds within a kind and at most one run is parked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The declaring extension namespaces every workflow's kind (registered by the loader before extension modules import), so extension resolves from the class: start() loses its extension argument, the DBOS attributes stamp subject only, lifecycle events derive their extension from the run's kind, and the transcript file listing returns names the client composes into URLs it already knows. Deploy note: _entry dropped its third argument — drain or cancel in-flight runs before deploying; DBOS recovery re-invokes recorded arguments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…names An aliased entry point (key != Extension.name) no longer poisons the package ownership map: the pre-import claim is provisional — released when the load fails, and an already-held claim is never touched, so the name-mismatch check raises the promised MalformedExtension in both orders. The two transcript-files tests missed in the url removal now assert names and compose the download URL the way the client does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
czpython
changed the base branch from
commonzenpython/eng-630-sdk-adopt-dbos-custom-workflow-attributes-for-subject-keying
to
main
July 13, 2026 15:54
Subject is routing metadata and rides before the payload everywhere else (start, _execute_run, the event emitters) — the entry chain was the one deviator, flipped inside the same compatibility window the arity change already opened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry raising ValueError forced the loader to catch-and-rebrand at a distance; a conflicting claim IS a packaging mistake, so the registry raises the loader's taxonomy itself and _resolve flattens to a bare with-block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entry modules never declare workflows — capability modules import at discover()/load(app), after the loader returns — so nothing needs the package registered before the entry loads. Registering the validated name with the authoritative Extension.package deletes the claim context manager, the derived-package guess, and the two-pass boot loop, and a failed load registers nothing by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package map is the loader-validated install claim, so it moves into the loader — registry.py returns to the capability catalogs it always was, and workflows.py asks the loader who owns a module. iter_extensions() gains the entry-key/name check _resolve() already had: production boot no longer accepts an aliased entry the app-less path rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generic-subjects seed and the scope-dispatch assertion still carried the per-run extension; the duplicate-name boot test now aliases both entries to one key, since per-entry name validation fires before duplication is observable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #3 (folded in — this PR now carries the whole arc against main). Two moves that end in one place: identity lives where it's declared, not on the run.
What changed
start()stampssubject_type/subject_idviaSetWorkflowAttributes,subject_filter()answers "runs for this subject" offworkflow_statusalone, anddurable_runssheds itssubject/input/extensioncolumns (bare drops, no backfill — pre-2.26 hosts don't exist). Extensions read runs throughRun.list_for_subject(..., kind=), neverdruks.durableinternals. DBOS workflow-status GC/retention must stay off.Extension.package) before importing extension modules;Workflow.__init_subclass__resolvescls.extensionfrom the registry and fails loudly for an unowned module, with the loader invariant in the message.kind, universally. In-tree kinds are byte-identical (build.*,core.*,usage.*— pinned by test). Out-of-tree kinds gain their namespace (field_notes.summarize), closing the collision where two external extensions sharing a local class name collided on settings keys, dedup slots, DBOS workflow names, and step prefixes. Explicitkindis a local suffix; dotted values are rejected.start(extension=), theattributes["extension"]stamp, theself.extensioninstance attr, the_emit_run_event/_log_run_eventthreading, andRun.extension(the647bc7f4read-back) are all gone. Lifecycle events derive their extension from the run's kind through the registry inside the memoized_transitionstep — deterministic class metadata, replay-safe.(extension, callId); it now composes file URLs the same way (subjectApi.transcriptFile).AgentCallFiles.from_callis a pure filesystem inventory — no data reads in the schema method.Plan deviation
The plan seeded
druks.core/druks.usageas framework packages resolving toNone— but core and usage are builtin extensions with entry points, so they register through the same flow as build and their workflows resolveextension == "core"/"usage". No seeding, no special case;Noneremains only for explicitly registered unowned packages (test modules).Deploy requirement
_entrydropped its third argument. DBOS recovery re-invokes recorded arguments, so no runs may be in flight when this deploys — drain or cancel live runs first. In-tree kinds and step names are unchanged, so completed checkpoints stay replay-compatible.Historical
attributes["extension"]values in DBOS rows become ignored write-orphans; no migration.🤖 Generated with Claude Code